/****************************************************************************************
*	Mouzetrap - client/server framework	
*
*	$HeadURL: http://mouzetrap.googlecode.com/svn/trunk/Mouzetrap/Network/BaseClient.cs $
*	$Id: BaseClient.cs 11 2009-06-11 09:10:25Z tobz@fallenrealms.net $
* 
*	Copyright (c) 2008 Toby Lawrence
* 
*	Permission is hereby granted, free of charge, to any person obtaining a copy
*	of this software and associated documentation files (the "Software"), to deal
*	in the Software without restriction, including without limitation the rights
*	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
*	copies of the Software, and to permit persons to whom the Software is
*	furnished to do so, subject to the following conditions:
* 
*	The above copyright notice and this permission notice shall be included in
*	all copies or substantial portions of the Software.
* 
*	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
*	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
*	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
*	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
*	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
*	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
*	THE SOFTWARE.
* 
****************************************************************************************/

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Mouzetrap.Localization;

namespace Mouzetrap.Network
{
	/// <summary>
	/// Defines the base functionality for a client.  This includes the sending and receiving of data.
	/// </summary>
	/// <typeparam name="TServer">the type of the server this client is associated with</typeparam>
	public abstract class BaseClient<TServer> : DisposableObject, IBaseClient<TServer>
		where TServer : BaseServer
	{
		private static readonly BufferPool BufferPool;
		private static long _globalBytesReceived;
		private static long _globalBytesSent;

		private readonly List<ArraySegment<byte>> _storedBuffers;
		private long _bytesReceived;
		private long _bytesSent;
		private int _bytesWaiting;
		private ArraySegment<byte> _receiveBuffer;
		private int _receiveOffset;
		private Socket _tcpSocket;

		static BaseClient()
		{
			BufferPool = new BufferPool(NetworkConstants.DEFAULT_PBUF_COUNT, NetworkConstants.DEFAULT_PBUF_SIZE);
		}

		protected BaseClient(TServer server, Socket clientSocket)
		{
			Server = server;

			TcpSocket = clientSocket;
			_storedBuffers = new List<ArraySegment<byte>>();
		}

		/// <summary>
		/// Gets how many bytes the client has been sent.
		/// </summary>
		public long BytesSent
		{
			get { return _bytesSent; }
			protected set { _bytesSent = value; }
		}

		/// <summary>
		/// Gets how many bytes the client has sent.
		/// </summary>
		public long BytesReceived
		{
			get { return _bytesReceived; }
			protected set { _bytesReceived = value; }
		}

		/// <summary>
		/// Gets how many bytes have been sent to all clients.
		/// </summary>
		public static long GlobalBytesSent
		{
			get { return Thread.VolatileRead(ref _globalBytesSent); }
		}

		/// <summary>
		/// Gets how many bytes all clients have sent.
		/// </summary>
		public static long GlobalBytesReceived
		{
			get { return Thread.VolatileRead(ref _globalBytesReceived); }
		}

		#region IBaseClient<TServer> Members

		/// <summary>
		/// Gets the server instance the client is being served by.
		/// </summary>
		public TServer Server { get; protected set; }

		/// <summary>
		/// Gets the TCP endpoint of the client.
		/// </summary>
		public IPEndPoint TcpEndpoint { get; protected set; }

		/// <summary>
		/// Gets the UDP endpoint of the client.
		/// </summary>
		public IPEndPoint UdpEndpoint { get; protected set; }

		/// <summary>
		/// Gets/sets the socket this client is using for TCP communication.
		/// </summary>
		public Socket TcpSocket
		{
			get { return _tcpSocket; }
			protected set
			{
				if(_tcpSocket != null && _tcpSocket.Connected)
				{
					_tcpSocket.Shutdown(SocketShutdown.Both);
					_tcpSocket.Close();
				}

				if(value != null)
				{
					_tcpSocket = value;
				}
			}
		}

		/// <summary>
		/// Whether or not the TCP socket is still connected.
		/// </summary>
		public virtual bool IsConnected
		{
			get { return _tcpSocket != null && _tcpSocket.Connected; }
		}

		/// <summary>
		/// Begins receiving TCP data asynchronously.
		/// </summary>
		public void BeginReceive()
		{
			ResumeReceive();
		}

		/// <summary>
		/// Asynchronously sends a packet of data to the client.
		/// </summary>
		/// <param name="buffer">an array of bytes containing the data to be sent</param>
		public void Send(byte[] buffer)
		{
			// we are sending the entire buffer.
			Send(buffer, 0, buffer.Length);
		}

		/// <summary>
		/// Asynchronously sends a packet of data to the client.
		/// </summary>
		/// <param name="segment">the buffer of data to read from</param>
		/// <param name="length">the number of bytes to send, starting at offset</param>
		public void Send(ArraySegment<byte> segment, int length)
		{
			// check length to make sure we have enough data to send
			if(length > segment.Count)
				throw new ArgumentOutOfRangeException("length", Mouzetrap_Messages.SegmentLength);

			// we are sending from the beginning of the segment.
			Send(segment.Array, segment.Offset, length);
		}

		/// <summary>
		/// Asynchronously sends a packet of data to the client.
		/// </summary>
		/// <param name="buffer">an array of bytes containing the data to be sent</param>
		/// <param name="length">the number of bytes to send, starting at offset</param>
		/// <param name="offset">the offset in the buffer to read from</param>
		public void Send(byte[] buffer, int offset, int length)
		{
			// make sure we're connected
			if(_tcpSocket != null && _tcpSocket.Connected)
			{
				SocketAsyncEventArgs args = NetworkHelper.AcquireSocketArgs();

				// make sure we have args
				if(args == null)
					throw new InvalidOperationException(Mouzetrap_Messages.SocketArgsNull);

				// set callback and data to send
				args.Completed += SendAsyncComplete;
				args.SetBuffer(buffer, offset, length);

				// send data
				_tcpSocket.SendAsync(args);

				// update data sent counters
				_bytesSent += length;
				Interlocked.Add(ref _globalBytesSent, length);
			}
		}

		/// <summary>
		/// Handle a packet.
		/// </summary>
		/// <param name="packet">the packet to handle</param>
		public virtual void Handle(IPacket packet)
		{
			// here is where you would handle routing the packet to 
			// different layers in your server, etc
		}

		#endregion

		private static void SendAsyncComplete(object sender, SocketAsyncEventArgs args)
		{
			args.Completed -= SendAsyncComplete;
			NetworkHelper.ReleaseSocketArgs(args);
		}

		/// <summary>
		/// Resumes asynchronous TCP receiving for this client.
		/// </summary>
		private void ResumeReceive()
		{
			// make sure we're connected
			if(_tcpSocket != null && _tcpSocket.Connected)
			{
				SocketAsyncEventArgs args = NetworkHelper.AcquireSocketArgs();

				// make sure we have args
				if(args == null)
					throw new InvalidOperationException(Mouzetrap_Messages.SocketArgsNull);

				// set callback to receive buffer
				args.Completed += ReceiveAsyncComplete;
				args.SetBuffer(_receiveBuffer.Array, _receiveBuffer.Offset + _receiveOffset,
				               _receiveBuffer.Count - _receiveOffset);

				// check if we already have data waiting, if so, process immediately
				bool willRaiseEvent = _tcpSocket.ReceiveAsync(args);
				if(!willRaiseEvent)
				{
					ProcessRecieve(args);
				}
			}
		}

		private void ProcessRecieve(SocketAsyncEventArgs args)
		{
			try
			{
				int bytesReceived = args.BytesTransferred;

				if(bytesReceived == 0)
					//if (args.SocketError != SocketError.Success)
				{
					// no bytes means the client disconnected, so clean up!
					//Server.DisconnectClient(this, true);
				}
				else
				{
					// update data received counters
					_bytesReceived += bytesReceived;
					Interlocked.Add(ref _globalBytesReceived, bytesReceived);

					// increment our total amount of data waiting
					_bytesWaiting += bytesReceived;

					bool releaseBuffer = OnReceive(_receiveBuffer, _bytesWaiting);

					// release the buffer, the client got all the data they needed
					if(releaseBuffer)
					{
						// reset offset/buffer
						_receiveOffset = 0;
						_bytesWaiting = 0;

						// return any buffers we're holding
						_storedBuffers.ForEach((buffer) => BufferPool.ReturnBuffer(buffer));
						_storedBuffers.Clear();

						// get a new buffer
						BufferPool.ReturnBuffer(_receiveBuffer);
						_receiveBuffer = BufferPool.GetBuffer();
					}
					else
					{
						// the client has instructed us to hold onto this buffer and to keep reading more
						// check to make sure we can establish a valid offset, or allocate a new buffer
						// if we've run out of space
						if(_receiveOffset + bytesReceived == _receiveBuffer.Count)
						{
							// we don't have room left in our current buffer.
							_storedBuffers.Add(_receiveBuffer);
							_receiveBuffer = BufferPool.GetBuffer();

							_receiveOffset = 0;
						}
						else
						{
							// we have space left in the buffer, adjust our offset
							_receiveOffset += bytesReceived;
						}
					}

					ResumeReceive();
				}
			}
			catch(ObjectDisposedException)
			{
				//Server.DisconnectClient(this, true);
			}
			catch(Exception e)
			{
				//Server.Warning(this, e);
				//Server.DisconnectClient(this, true);
			}
			finally
			{
				args.Completed -= ReceiveAsyncComplete;
				NetworkHelper.ReleaseSocketArgs(args);
			}
		}

		private void ReceiveAsyncComplete(object sender, SocketAsyncEventArgs args)
		{
			ProcessRecieve(args);
		}

		protected abstract bool OnReceive(ArraySegment<byte> segment, int received);

		protected byte[] ReadReceivedData()
		{
			// we need to allocate a buffer to hold all stored buffer data, and the current buffer
			var tmpBuf = new byte[_bytesWaiting];
			int offset = 0;

			// copy all stored buffers in order
			foreach(var segment in _storedBuffers)
			{
				Buffer.BlockCopy(segment.Array, segment.Offset, tmpBuf, offset, segment.Count);
				offset += segment.Count;
			}

			// copy the current buffer
			Buffer.BlockCopy(_receiveBuffer.Array, _receiveBuffer.Offset, tmpBuf,
			                 offset, (_receiveBuffer.Count - _receiveOffset));

			return tmpBuf;
		}
	}
}